/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2012 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.mail.pop3;

import java.io.*;
import java.util.Enumeration;
import java.util.logging.Level;
import java.lang.ref.SoftReference;
import javax.mail.*;
import javax.mail.internet.*;
import javax.mail.event.*;
import com.sun.mail.util.ReadableMime;

/**
 * A POP3 Message. Just like a MimeMessage except that
 * some things are not supported.
 * 
 * @author Bill Shannon
 */
public class POP3Message extends MimeMessage implements ReadableMime {

	/*
	 * Our locking strategy is to always lock the POP3Folder before the
	 * POP3Message so we have to be careful to drop our lock before calling
	 * back to the folder to close it and notify of connection lost events.
	 */

	// flag to indicate we haven't tried to fetch the UID yet
	static final String UNKNOWN = "UNKNOWN";

	private POP3Folder folder; // overrides folder in MimeMessage
	private int hdrSize = -1;
	private int msgSize = -1;
	String uid = UNKNOWN; // controlled by folder lock

	// rawData itself is never null
	private SoftReference rawData = new SoftReference(null);

	public POP3Message(Folder folder, int msgno) throws MessagingException {
		super(folder, msgno);
		this.folder = (POP3Folder) folder;
	}

	/**
	 * Set the specified flags on this message to the specified value.
	 * 
	 * @param newFlags
	 *            the flags to be set
	 * @param set
	 *            the value to be set
	 */
	public synchronized void setFlags(Flags newFlags, boolean set) throws MessagingException {
		Flags oldFlags = (Flags) flags.clone();
		super.setFlags(newFlags, set);
		if (!flags.equals(oldFlags))
			folder.notifyMessageChangedListeners(MessageChangedEvent.FLAGS_CHANGED, this);
	}

	/**
	 * Return the size of the content of this message in bytes.
	 * Returns -1 if the size cannot be determined.
	 * <p>
	 * 
	 * Note that this number may not be an exact measure of the content size and
	 * may or may not account for any transfer encoding of the content.
	 * <p>
	 * 
	 * @return size of content in bytes
	 * @exception MessagingException
	 */
	public int getSize() throws MessagingException {
		try {
			synchronized (this) {
				// if we already have the size, return it
				if (msgSize > 0)
					return msgSize;
			}

			/*
			 * Use LIST to determine the entire message
			 * size and subtract out the header size
			 * (which may involve loading the headers,
			 * which may load the content as a side effect).
			 * If the content is loaded as a side effect of
			 * loading the headers, it will set the size.
			 * 
			 * Make sure to call loadHeaders() outside of the
			 * synchronization block. There's a potential race
			 * condition here but synchronization will occur in
			 * loadHeaders() to make sure the headers are only
			 * loaded once, and again in the following block to
			 * only compute msgSize once.
			 */
			if (headers == null)
				loadHeaders();

			synchronized (this) {
				if (msgSize < 0)
					msgSize = folder.getProtocol().list(msgnum) - hdrSize;
				return msgSize;
			}
		} catch (EOFException eex) {
			folder.close(false);
			throw new FolderClosedException(folder, eex.toString());
		} catch (IOException ex) {
			throw new MessagingException("error getting size", ex);
		}
	}

	/**
	 * Produce the raw bytes of the message. The data is fetched using
	 * the POP3 RETR command. If skipHeader is true, just the content
	 * is returned.
	 */
	private InputStream getRawStream(boolean skipHeader) throws MessagingException {
		InputStream rawcontent = null;
		try {
			synchronized (this) {
				rawcontent = (InputStream) rawData.get();
				if (rawcontent == null) {
					TempFile cache = folder.getFileCache();
					if (cache != null) {
						Session s = ((POP3Store) (folder.getStore())).getSession();
						if (folder.logger.isLoggable(Level.FINE))
							folder.logger.fine("caching message #" + msgnum + " in temp file");
						AppendStream os = cache.getAppendStream();
						BufferedOutputStream bos = new BufferedOutputStream(os);
						try {
							folder.getProtocol().retr(msgnum, bos);
						} finally {
							bos.close();
						}
						rawcontent = os.getInputStream();
					} else {
						rawcontent = folder.getProtocol().retr(msgnum, msgSize > 0 ? msgSize + hdrSize : 0);
					}
					if (rawcontent == null) {
						expunged = true;
						throw new MessageRemovedException("can't retrieve message #" + msgnum + " in POP3Message.getContentStream"); // XXX
																																		// -
																																		// what
																																		// else?
					}

					if (headers == null || ((POP3Store) (folder.getStore())).forgetTopHeaders) {
						headers = new InternetHeaders(rawcontent);
						hdrSize = (int) ((SharedInputStream) rawcontent).getPosition();
					} else {
						/*
						 * Already have the headers, have to skip the headers
						 * in the content array and return the body.
						 * 
						 * XXX - It seems that some mail servers return slightly
						 * different headers in the RETR results than were
						 * returned
						 * in the TOP results, so we can't depend on remembering
						 * the size of the headers from the TOP command and just
						 * skipping that many bytes. Instead, we have to process
						 * the content, skipping over the header until we come
						 * to
						 * the empty line that separates the header from the
						 * body.
						 */
						int offset = 0;
						for (;;) {
							int len = 0; // number of bytes in this line
							int c1;
							while ((c1 = rawcontent.read()) >= 0) {
								if (c1 == '\n') // end of line
									break;
								else if (c1 == '\r') {
									// got CR, is the next char LF?
									if (rawcontent.available() > 0) {
										rawcontent.mark(1);
										if (rawcontent.read() != '\n')
											rawcontent.reset();
									}
									break; // in any case, end of line
								}

								// not CR, NL, or CRLF, count the byte
								len++;
							}
							// here when end of line or out of data

							// if out of data, we're done
							if (rawcontent.available() == 0)
								break;

							// if it was an empty line, we're done
							if (len == 0)
								break;
						}
						hdrSize = (int) ((SharedInputStream) rawcontent).getPosition();
					}

					// skipped the header, the message is what's left
					msgSize = rawcontent.available();

					rawData = new SoftReference(rawcontent);
				}
			}
		} catch (EOFException eex) {
			folder.close(false);
			throw new FolderClosedException(folder, eex.toString());
		} catch (IOException ex) {
			throw new MessagingException("error fetching POP3 content", ex);
		}

		/*
		 * We have a cached stream, but we need to return
		 * a fresh stream to read from the beginning and
		 * that can be safely closed.
		 */
		rawcontent = ((SharedInputStream) rawcontent).newStream(skipHeader ? hdrSize : 0, -1);
		return rawcontent;
	}

	/**
	 * Produce the raw bytes of the content. The data is fetched using
	 * the POP3 RETR command.
	 * 
	 * @see #contentStream
	 */
	protected synchronized InputStream getContentStream() throws MessagingException {
		if (contentStream != null)
			return ((SharedInputStream) contentStream).newStream(0, -1);

		InputStream cstream = getRawStream(true);

		/*
		 * Keep a hard reference to the data if we're using a file
		 * cache or if the "mail.pop3.keepmessagecontent" prop is set.
		 */
		TempFile cache = folder.getFileCache();
		if (cache != null || ((POP3Store) (folder.getStore())).keepMessageContent)
			contentStream = ((SharedInputStream) cstream).newStream(0, -1);
		return cstream;
	}

	/**
	 * Return the MIME format stream corresponding to this message part.
	 * 
	 * @return the MIME format stream
	 * @since JavaMail 1.4.5
	 */
	public InputStream getMimeStream() throws MessagingException {
		return getRawStream(false);
	}

	/**
	 * Invalidate the cache of content for this message object, causing
	 * it to be fetched again from the server the next time it is needed.
	 * If <code>invalidateHeaders</code> is true, invalidate the headers
	 * as well.
	 * 
	 * @param invalidateHeaders
	 *            invalidate the headers as well?
	 */
	public synchronized void invalidate(boolean invalidateHeaders) {
		content = null;
		InputStream rstream = (InputStream) rawData.get();
		if (rstream != null) {
			// note that if the content is in the file cache, it will be lost
			// and fetched from the server if it's needed again
			try {
				rstream.close();
			} catch (IOException ex) {
				// ignore it
			}
			rawData = new SoftReference(null);
		}
		if (contentStream != null) {
			try {
				contentStream.close();
			} catch (IOException ex) {
				// ignore it
			}
			contentStream = null;
		}
		msgSize = -1;
		if (invalidateHeaders) {
			headers = null;
			hdrSize = -1;
		}
	}

	/**
	 * Fetch the header of the message and the first <code>n</code> lines
	 * of the raw content of the message. The headers and data are
	 * available in the returned InputStream.
	 * 
	 * @param n
	 *            number of lines of content to fetch
	 * @return InputStream containing the message headers and n content lines
	 */
	public InputStream top(int n) throws MessagingException {
		try {
			synchronized (this) {
				return folder.getProtocol().top(msgnum, n);
			}
		} catch (EOFException eex) {
			folder.close(false);
			throw new FolderClosedException(folder, eex.toString());
		} catch (IOException ex) {
			throw new MessagingException("error getting size", ex);
		}
	}

	/**
	 * Get all the headers for this header_name. Note that certain
	 * headers may be encoded as per RFC 2047 if they contain
	 * non US-ASCII characters and these should be decoded.
	 * <p>
	 * 
	 * @param name
	 *            name of header
	 * @return array of headers
	 * @exception MessagingException
	 * @see javax.mail.internet.MimeUtility
	 */
	public String[] getHeader(String name) throws MessagingException {
		if (headers == null)
			loadHeaders();
		return headers.getHeader(name);
	}

	/**
	 * Get all the headers for this header name, returned as a single
	 * String, with headers separated by the delimiter. If the
	 * delimiter is <code>null</code>, only the first header is
	 * returned.
	 * 
	 * @param name
	 *            the name of this header
	 * @param delimiter
	 *            delimiter between returned headers
	 * @return the value fields for all headers with
	 *         this name
	 * @exception MessagingException
	 */
	public String getHeader(String name, String delimiter) throws MessagingException {
		if (headers == null)
			loadHeaders();
		return headers.getHeader(name, delimiter);
	}

	/**
	 * Set the value for this header_name. Throws IllegalWriteException
	 * because POP3 messages are read-only.
	 * 
	 * @param name
	 *            header name
	 * @param value
	 *            header value
	 * @see javax.mail.internet.MimeUtility
	 * @exception IllegalWriteException
	 *                because the underlying
	 *                implementation does not support modification
	 * @exception IllegalStateException
	 *                if this message is
	 *                obtained from a READ_ONLY folder.
	 */
	public void setHeader(String name, String value) throws MessagingException {
		// XXX - should check for read-only folder?
		throw new IllegalWriteException("POP3 messages are read-only");
	}

	/**
	 * Add this value to the existing values for this header_name.
	 * Throws IllegalWriteException because POP3 messages are read-only.
	 * 
	 * @param name
	 *            header name
	 * @param value
	 *            header value
	 * @see javax.mail.internet.MimeUtility
	 * @exception IllegalWriteException
	 *                because the underlying
	 *                implementation does not support modification
	 * @exception IllegalStateException
	 *                if this message is
	 *                obtained from a READ_ONLY folder.
	 */
	public void addHeader(String name, String value) throws MessagingException {
		// XXX - should check for read-only folder?
		throw new IllegalWriteException("POP3 messages are read-only");
	}

	/**
	 * Remove all headers with this name.
	 * Throws IllegalWriteException because POP3 messages are read-only.
	 * 
	 * @exception IllegalWriteException
	 *                because the underlying
	 *                implementation does not support modification
	 * @exception IllegalStateException
	 *                if this message is
	 *                obtained from a READ_ONLY folder.
	 */
	public void removeHeader(String name) throws MessagingException {
		// XXX - should check for read-only folder?
		throw new IllegalWriteException("POP3 messages are read-only");
	}

	/**
	 * Return all the headers from this Message as an enumeration
	 * of Header objects.
	 * <p>
	 * 
	 * Note that certain headers may be encoded as per RFC 2047 if they contain
	 * non US-ASCII characters and these should be decoded.
	 * <p>
	 * 
	 * @return array of header objects
	 * @exception MessagingException
	 * @see javax.mail.internet.MimeUtility
	 */
	public Enumeration getAllHeaders() throws MessagingException {
		if (headers == null)
			loadHeaders();
		return headers.getAllHeaders();
	}

	/**
	 * Return matching headers from this Message as an Enumeration of
	 * Header objects.
	 * 
	 * @exception MessagingException
	 */
	public Enumeration getMatchingHeaders(String[] names) throws MessagingException {
		if (headers == null)
			loadHeaders();
		return headers.getMatchingHeaders(names);
	}

	/**
	 * Return non-matching headers from this Message as an
	 * Enumeration of Header objects.
	 * 
	 * @exception MessagingException
	 */
	public Enumeration getNonMatchingHeaders(String[] names) throws MessagingException {
		if (headers == null)
			loadHeaders();
		return headers.getNonMatchingHeaders(names);
	}

	/**
	 * Add a raw RFC822 header-line.
	 * Throws IllegalWriteException because POP3 messages are read-only.
	 * 
	 * @exception IllegalWriteException
	 *                because the underlying
	 *                implementation does not support modification
	 * @exception IllegalStateException
	 *                if this message is
	 *                obtained from a READ_ONLY folder.
	 */
	public void addHeaderLine(String line) throws MessagingException {
		// XXX - should check for read-only folder?
		throw new IllegalWriteException("POP3 messages are read-only");
	}

	/**
	 * Get all header lines as an Enumeration of Strings. A Header
	 * line is a raw RFC822 header-line, containing both the "name"
	 * and "value" field.
	 * 
	 * @exception MessagingException
	 */
	public Enumeration getAllHeaderLines() throws MessagingException {
		if (headers == null)
			loadHeaders();
		return headers.getAllHeaderLines();
	}

	/**
	 * Get matching header lines as an Enumeration of Strings.
	 * A Header line is a raw RFC822 header-line, containing both
	 * the "name" and "value" field.
	 * 
	 * @exception MessagingException
	 */
	public Enumeration getMatchingHeaderLines(String[] names) throws MessagingException {
		if (headers == null)
			loadHeaders();
		return headers.getMatchingHeaderLines(names);
	}

	/**
	 * Get non-matching header lines as an Enumeration of Strings.
	 * A Header line is a raw RFC822 header-line, containing both
	 * the "name" and "value" field.
	 * 
	 * @exception MessagingException
	 */
	public Enumeration getNonMatchingHeaderLines(String[] names) throws MessagingException {
		if (headers == null)
			loadHeaders();
		return headers.getNonMatchingHeaderLines(names);
	}

	/**
	 * POP3 message can't be changed. This method throws
	 * IllegalWriteException.
	 * 
	 * @exception IllegalWriteException
	 *                because the underlying
	 *                implementation does not support modification
	 */
	public void saveChanges() throws MessagingException {
		// POP3 Messages are read-only
		throw new IllegalWriteException("POP3 messages are read-only");
	}

	/**
	 * Output the message as an RFC 822 format stream, without
	 * specified headers. If the property "mail.pop3.cachewriteto"
	 * is set to "true", and ignoreList is null, and the message hasn't
	 * already been cached as a side effect of other operations, the message
	 * content is cached before being written. Otherwise, the message is
	 * streamed directly to the output stream without being cached.
	 * 
	 * @exception javax.mail.MessagingException
	 * @exception IOException
	 *                if an error occurs writing to the stream
	 *                or if an error is generated by the
	 *                javax.activation layer.
	 * @see javax.activation.DataHandler#writeTo
	 */
	public synchronized void writeTo(OutputStream os, String[] ignoreList) throws IOException, MessagingException {
		InputStream rawcontent = (InputStream) rawData.get();
		if (rawcontent == null && ignoreList == null && !((POP3Store) (folder.getStore())).cacheWriteTo) {
			Session s = ((POP3Store) (folder.getStore())).getSession();
			if (folder.logger.isLoggable(Level.FINE))
				folder.logger.fine("streaming msg " + msgnum);
			if (!folder.getProtocol().retr(msgnum, os)) {
				expunged = true;
				throw new MessageRemovedException("can't retrieve message #" + msgnum + " in POP3Message.writeTo"); // XXX
																													// -
																													// what
																													// else?
			}
		} else if (rawcontent != null && ignoreList == null) {
			// can just copy the cached data
			InputStream in = ((SharedInputStream) rawcontent).newStream(0, -1);
			try {
				byte[] buf = new byte[16 * 1024];
				int len;
				while ((len = in.read(buf)) > 0)
					os.write(buf, 0, len);
			} finally {
				try {
					if (in != null)
						in.close();
				} catch (IOException ex) {
				}
			}
		} else
			super.writeTo(os, ignoreList);
	}

	/**
	 * Load the headers for this message into the InternetHeaders object.
	 * The headers are fetched using the POP3 TOP command.
	 */
	private void loadHeaders() throws MessagingException {
		assert !Thread.holdsLock(this);
		try {
			boolean fetchContent = false;
			synchronized (this) {
				if (headers != null) // check again under lock
					return;
				InputStream hdrs = null;
				if (((POP3Store) (folder.getStore())).disableTop || (hdrs = folder.getProtocol().top(msgnum, 0)) == null) {
					// possibly because the TOP command isn't supported,
					// load headers as a side effect of loading the entire
					// content.
					fetchContent = true;
				} else {
					try {
						hdrSize = hdrs.available();
						headers = new InternetHeaders(hdrs);
					} finally {
						hdrs.close();
					}
				}
			}

			/*
			 * Outside the synchronization block...
			 * 
			 * Do we need to fetch the entire mesage content in order to
			 * load the headers as a side effect? Yes, there's a race
			 * condition here - multiple threads could decide that the
			 * content needs to be fetched. Fortunately, they'll all
			 * synchronize in the getContentStream method and the content
			 * will only be loaded once.
			 */
			if (fetchContent) {
				InputStream cs = null;
				try {
					cs = getContentStream();
				} finally {
					if (cs != null)
						cs.close();
				}
			}
		} catch (EOFException eex) {
			folder.close(false);
			throw new FolderClosedException(folder, eex.toString());
		} catch (IOException ex) {
			throw new MessagingException("error loading POP3 headers", ex);
		}
	}
}
